/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.seed.launcher.listener.wifi;

import android.Manifest;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkInfo;
import android.net.NetworkRequest;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.Log;


import androidx.core.app.ActivityCompat;

import com.seed.launcher.listener.util.ThreadUtils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

public class WifiTracker {

    //region 成员变量

    /**
     * Default maximum age in millis of cached scored networks in
     */
    private static final long DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS = 20 * DateUtils.MINUTE_IN_MILLIS;

    /**
     * Maximum age of scan results to hold onto while actively scanning.
     **/
    static final long MAX_SCAN_RESULT_AGE_MILLIS = 15000;

    private static final String TAG = "WifiTracker";

    private OnWifiChangedListener mOnWifiChangedListener;
    private final com.seed.launcher.listener.bean.WifiInfo currentWifiInfo = new com.seed.launcher.listener.bean.WifiInfo();
    private final List<com.seed.launcher.listener.bean.WifiInfo> wifiList = new ArrayList<>();

    public WifiTracker(Context context, OnWifiChangedListener listener) {
        this(context);
        mOnWifiChangedListener = listener;
    }

    private static final boolean DBG() {
        return Log.isLoggable(TAG, Log.DEBUG);
    }

    private static boolean isVerboseLoggingEnabled() {
        return WifiTracker.sVerboseLogging || Log.isLoggable(TAG, Log.VERBOSE);
    }

    /**
     * Verbose logging flag set thru developer debugging options and used so as to assist with
     * in-the-field WiFi connectivity debugging.
     *
     * <p>{@link #isVerboseLoggingEnabled()} should be read rather than referencing this value
     * directly, to ensure adb TAG level verbose settings are respected.
     */
    public static boolean sVerboseLogging = false;

    // TODO: Allow control of this?
    // Combo scans can take 5-6s to complete - set to 10s.
    private static final int WIFI_RESCAN_INTERVAL_MS = 10 * 1000;

    private Context mContext;
    private WifiManager mWifiManager;
    private IntentFilter mFilter;
    private ConnectivityManager mConnectivityManager;
    private NetworkRequest mNetworkRequest;
    private AtomicBoolean mConnected = new AtomicBoolean(false);
    private WifiListenerExecutor mListener;

    Handler mWorkHandler;
    private HandlerThread mWorkThread;

    private WifiTrackerNetworkCallback mNetworkCallback;

    /**
     * Synchronization lock for managing concurrency between main and worker threads.
     *
     * <p>This lock should be held for all modifications to  #mInternalAccessPoints} and
     * {@link #mScanner}.
     */
    private final Object mLock = new Object();


    /**
     * Tracks whether fresh scan results have been received since scanning start.
     *
     * <p>If this variable is false, we will not invoke callbacks so that we do not
     * update the UI with stale data / clear out existing UI elements prematurely.
     */
    private boolean mStaleScanResults = true;

    /**
     * Tracks whether the latest SCAN_RESULTS_AVAILABLE_ACTION contained new scans. If not, then
     * we treat the last scan as an aborted scan and increase the eviction timeout window to avoid
     * completely flushing the AP list before the next successful scan completes.
     */
    private boolean mLastScanSucceeded = true;

    // Does not need to be locked as it only updated on the worker thread, with the exception of
    // during onStart, which occurs before the receiver is registered on the work handler.
    private final HashMap<String, ScanResult> mScanResultCache = new HashMap<>();
    private boolean mRegistered;

    private NetworkInfo mLastNetworkInfo;
    private WifiInfo mLastInfo;

    private boolean mNetworkScoringUiEnabled;
    private long mMaxSpeedLabelScoreCacheAge;

    private static final String WIFI_SECURITY_PSK = "PSK";
    private static final String WIFI_SECURITY_EAP = "EAP";
    private static final String WIFI_SECURITY_SAE = "SAE";
    private static final String WIFI_SECURITY_OWE = "OWE";
    private static final String WIFI_SECURITY_SUITE_B_192 = "SUITE_B_192";
    //endregion

    Scanner mScanner;

    private static IntentFilter newIntentFilter() {
        IntentFilter filter = new IntentFilter();
        filter.addAction(WifiManager.WIFI_STATE_CHANGED_ACTION);
        filter.addAction(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION);
        filter.addAction(WifiManager.NETWORK_IDS_CHANGED_ACTION);
        filter.addAction(WifiManager.SUPPLICANT_STATE_CHANGED_ACTION);
//        filter.addAction(WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION);
//        filter.addAction(WifiManager.ACTION_LINK_CONFIGURATION_CHANGED);
        filter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
        filter.addAction(WifiManager.RSSI_CHANGED_ACTION);

        return filter;
    }


    public WifiTracker(Context context) {
        this(context, new WifiListener() {
            @Override
            public void onWifiStateChanged(int state) {

            }

            @Override
            public void onConnectedChanged() {

            }

            @Override
            public void onAccessPointsChanged() {

            }
        });
    }

    public WifiTracker(Context context, WifiListener wifiListener) {
        this(context, wifiListener,
                ((WifiManager) context.getApplicationContext()
                        .getSystemService(Context.WIFI_SERVICE)),
                ((ConnectivityManager) context.getApplicationContext()
                        .getSystemService(Context.CONNECTIVITY_SERVICE)),
                newIntentFilter());
    }


    WifiTracker(Context context, WifiListener wifiListener,
                WifiManager wifiManager, ConnectivityManager connectivityManager,
                IntentFilter filter) {
        mContext = context;
        mWifiManager = wifiManager;
        mListener = new WifiListenerExecutor(wifiListener);
        mConnectivityManager = connectivityManager;

        mFilter = filter;

        mNetworkRequest = new NetworkRequest.Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .build();

        // TODO(sghuman): Remove this and create less hacky solution for testing
        final HandlerThread workThread = new HandlerThread(TAG
                + "{" + Integer.toHexString(System.identityHashCode(this)) + "}",
                Process.THREAD_PRIORITY_BACKGROUND);
        workThread.start();
        setWorkThread(workThread);
    }

    /**
     * Validity warning: this wipes out mScoreCache, so use with extreme caution
     *
     * @param workThread substitute Handler thread, for testing purposes only
     */
    // TODO(sghuman): Remove this method, this needs to happen in a factory method and be passed in
    // during construction
    void setWorkThread(HandlerThread workThread) {
        mWorkThread = workThread;
        mWorkHandler = new Handler(workThread.getLooper());
    }

    public void onDestroy() {
        mWorkThread.quit();
    }

    /**
     * Temporarily stop scanning for wifi networks.
     *
     * <p>Sets {@link #mStaleScanResults} to true.
     */
    private void pauseScanning() {
        synchronized (mLock) {
            if (mScanner != null) {
                mScanner.pause();
                mScanner = null;
            }
        }
        mStaleScanResults = true;
    }

    /**
     * Resume scanning for wifi networks after it has been paused.
     *
     * <p>The score cache should be registered before this method is invoked.
     */
    public void resumeScanning() {
        synchronized (mLock) {
            if (mScanner == null) {
                mScanner = new Scanner();
            }

            if (isWifiEnabled()) {
                mScanner.resume();
            }
        }
    }


    public void onStart() {
        // fetch current ScanResults instead of waiting for broadcast of fresh results
        forceUpdate();
//        mNetworkScoringUiEnabled =
//                Settings.Global.getInt(
//                        mContext.getContentResolver(),
//                        Settings.Global.NETWORK_SCORING_UI_ENABLED, 0) == 1;

//        mMaxSpeedLabelScoreCacheAge =
//                Settings.Global.getLong(
//                        mContext.getContentResolver(),
//                        Settings.Global.SPEED_LABEL_CACHE_EVICTION_AGE_MILLIS,
//                        DEFAULT_MAX_CACHED_SCORE_AGE_MILLIS);

        resumeScanning();
        if (!mRegistered) {
            mContext.registerReceiver(mReceiver, mFilter, null, mWorkHandler);
            // NetworkCallback objects cannot be reused. http://b/20701525 .
            mNetworkCallback = new WifiTrackerNetworkCallback();
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                mConnectivityManager.registerNetworkCallback(
                        mNetworkRequest, mNetworkCallback, mWorkHandler);
            }
            mRegistered = true;
        }
    }


    /**
     * Synchronously update the list of access points with the latest information.
     *
     * <p>Intended to only be invoked within {@link #onStart()}.
     */
    void forceUpdate() {
        mLastInfo = mWifiManager.getConnectionInfo();
//        mLastNetworkInfo = mConnectivityManager.getNetworkInfo();
        Log.i(TAG, "forceUpdate: ");
        fetchScansAndConfigsAndUpdateAccessPoints();
    }


    /**
     * Stop tracking wifi networks and scores.
     *
     * <p>This should always be called when done with a WifiTracker (if onStart was called) to
     * ensure proper cleanup and prevent any further callbacks from occurring.
     *
     * <p>Calling this method will set the {@link #mStaleScanResults} bit, which prevents
     * {@link WifiListener#onAccessPointsChanged()} callbacks from being invoked (until the bit
     * is unset on the next SCAN_RESULTS_AVAILABLE_ACTION).
     */
    public void onStop() {
        if (mRegistered) {
            mContext.unregisterReceiver(mReceiver);
            mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
            mRegistered = false;
        }
        pauseScanning(); // and set mStaleScanResults

        mWorkHandler.removeCallbacksAndMessages(null /* remove all */);
    }


    public WifiManager getManager() {
        return mWifiManager;
    }

    public boolean isWifiEnabled() {
        return mWifiManager != null && mWifiManager.isWifiEnabled();
    }


    public boolean isConnected() {
        return mConnected.get();
    }

    //region 处理结果
    private ArrayMap<String, List<ScanResult>> updateScanResultCache(
            final List<ScanResult> newResults) {
        // TODO(sghuman): Delete this and replace it with the Map of Ap Keys to ScanResults for
        // memory efficiency
        for (ScanResult newResult : newResults) {
            if (newResult.SSID == null || newResult.SSID.isEmpty()) {
                continue;
            }
            mScanResultCache.put(newResult.BSSID, newResult);
        }

        // Evict old results in all conditions
        //过滤先前扫描到的热点
        evictOldScans();

        ArrayMap<String, List<ScanResult>> scanResultsByApKey = new ArrayMap<>();

        for (ScanResult result : mScanResultCache.values()) {
            // Ignore hidden and ad-hoc networks.
            if (result.SSID == null || result.SSID.length() == 0 ||
                    result.capabilities.contains("[IBSS]")) {
                continue;
            }

            String apKey = WifiUtil.getKey(result.SSID, result.BSSID, WifiUtil.getSecurity(mContext, result));
            List<ScanResult> resultList;
            if (scanResultsByApKey.containsKey(apKey)) {
                resultList = scanResultsByApKey.get(apKey);
            } else {
                resultList = new ArrayList<>();
                scanResultsByApKey.put(apKey, resultList);
            }
            resultList.add(result);
        }

        List<com.seed.launcher.listener.bean.WifiInfo> wifiInfoList = new ArrayList<>();
        boolean currentUpdated = false;
        com.seed.launcher.listener.bean.WifiInfo wifiInfo;
        for (String key : scanResultsByApKey.keySet()) {
            List<ScanResult> scanResults = scanResultsByApKey.get(key);
            if (scanResults == null || scanResults.isEmpty()) {
                continue;
            }
            ScanResult value = findTheMaxSignalScanResult(scanResults);
            if (TextUtils.isEmpty(value.SSID)) {
                continue;
            }
            wifiInfo = new com.seed.launcher.listener.bean.WifiInfo();
            wifiInfo.BSSID = value.BSSID;
            wifiInfo.SSID = value.SSID;
            wifiInfo.capabilities = value.capabilities;
            wifiInfo.frequency = value.frequency;
            wifiInfo.level = value.level;
            wifiInfo.timestamp = value.timestamp;
            wifiInfoList.add(wifiInfo);
            if (!currentUpdated) {
                currentUpdated = updateCurrent(wifiInfo);
            }
        }

        //更新当前wifi信息
        final boolean notifyCurrent = currentUpdated;
        if (mOnWifiChangedListener != null && notifyCurrent) {
            ThreadUtils.postOnMainThread(() -> {
                mOnWifiChangedListener.onCurrentWifiChanged(currentWifiInfo);
            });
        }

        String ssid = currentWifiInfo.SSID;
//        Log.i(TAG, "current BSSID: " + currentWifiInfo.BSSID);
//        Log.i(TAG, "current SSID: " + currentWifiInfo.SSID);
        Collections.sort(wifiInfoList, (o1, o2) -> {
            String o1SSID = o1.SSID;
            String o2SSID = o2.SSID;
            if (TextUtils.equals(o1SSID, ssid)) {
                return -1;
            }
            if (TextUtils.equals(o2SSID, ssid)) {
                return 1;
            }
            return o2.calculateSignalLevel(10) - o1.calculateSignalLevel(10);
        });
//        Log.i(TAG, "first BSSID: " + wifiInfoList.get(0).BSSID);
//        Log.i(TAG, "first SSID: " + wifiInfoList.get(0).SSID);
        wifiList.clear();
        wifiList.addAll(wifiInfoList);

        if (mOnWifiChangedListener != null) {
            ThreadUtils.postOnMainThread(() -> {
                mOnWifiChangedListener.onWifiListChanged(new ArrayList<>(wifiList));
            });
        }

        return scanResultsByApKey;
    }


    /**
     * 更新当前连接 wifi 信息
     */
    boolean updateCurrent(com.seed.launcher.listener.bean.WifiInfo info) {
        if (info == null) {
            return false;
        }
        if (TextUtils.equals(info.BSSID, currentWifiInfo.BSSID)) {
            if (currentWifiInfo.level == 0
                    || currentWifiInfo.capabilities == null
                    || currentWifiInfo.timestamp == 0) {

                currentWifiInfo.capabilities = info.capabilities;
                currentWifiInfo.level = info.level;
                currentWifiInfo.timestamp = info.timestamp;
                return true;
            }
        }
        return false;
    }


    /**
     * 寻找信号最好的接入点,当前连接的优先匹配
     */
    private ScanResult findTheMaxSignalScanResult(List<ScanResult> list) {
        ScanResult result = null;
        String curBSSID = currentWifiInfo.BSSID;
        for (ScanResult scanResult : list) {
            if (result == null) {
                result = scanResult;
            } else {
                if (TextUtils.equals(scanResult.BSSID, curBSSID)) {
                    return scanResult;
                }
                int levelResult = WifiManager.calculateSignalLevel(result.level, 10);
                int levelCurrent = WifiManager.calculateSignalLevel(scanResult.level, 10);
                if (levelCurrent > levelResult) {
                    result = scanResult;
                }
            }
        }
        return result;
    }

    /**
     * Remove old scan results from the cache. If {@link #mLastScanSucceeded} is false, then
     * increase the timeout window to avoid completely flushing the AP list before the next
     * successful scan completes.
     *
     * <p>Should only ever be invoked from {@link #updateScanResultCache(List)} when
     * {@link #mStaleScanResults} is false.
     */
    private void evictOldScans() {
        long evictionTimeoutMillis = mLastScanSucceeded ? MAX_SCAN_RESULT_AGE_MILLIS
                : MAX_SCAN_RESULT_AGE_MILLIS * 2;

        long nowMs = SystemClock.elapsedRealtime();
        for (Iterator<ScanResult> iter = mScanResultCache.values().iterator(); iter.hasNext(); ) {
            ScanResult result = iter.next();
            // result timestamp is in microseconds
            if (nowMs - result.timestamp / 1000 > evictionTimeoutMillis) {
                iter.remove();
            }
        }
    }

    private WifiConfiguration getWifiConfigurationForNetworkId(
            int networkId, final List<WifiConfiguration> configs) {
        if (configs != null) {
            for (WifiConfiguration config : configs) {
                if (mLastInfo != null && networkId == config.networkId) {
                    return config;
                }
            }
        }
        return null;
    }


    /**
     * Retrieves latest scan results and wifi configs, then calls
     * {@link #updateAccessPoints(List, List)}.
     */
    private void fetchScansAndConfigsAndUpdateAccessPoints() {
//        Log.i(TAG, "fetchScansAndConfigsAndUpdateAccessPoints: ");
        List<ScanResult> newScanResults = mWifiManager.getScanResults();

        // Filter all unsupported networks from the scan result list
        final List<ScanResult> filteredScanResults =
                filterScanResultsByCapabilities(newScanResults);

        if (isVerboseLoggingEnabled()) {
            Log.i(TAG, "Fetched scan results: " + filteredScanResults);
        }
        updateScanResultCache(newScanResults);

        if (ActivityCompat.checkSelfPermission(mContext, Manifest.permission.ACCESS_FINE_LOCATION)
                != PackageManager.PERMISSION_GRANTED) {
            return;
        }
        List<WifiConfiguration> configs = mWifiManager.getConfiguredNetworks();
        updateAccessPoints(filteredScanResults, configs);
    }

    /**
     * Update the internal list of access points.
     */
    private void updateAccessPoints(final List<ScanResult> newScanResults,
                                    List<WifiConfiguration> configs) {


        WifiConfiguration connectionConfig = null;
        if (mLastInfo != null) {
            connectionConfig = getWifiConfigurationForNetworkId(mLastInfo.getNetworkId(), configs);
        }
        conditionallyNotifyListeners();
    }

    private ScanResult findByWifiInfo(WifiInfo wifiInfo) {
        if (wifiInfo == null) {
            return null;
        }
        for (ScanResult scanResult : mWifiManager.getScanResults()) {
            if (TextUtils.equals(wifiInfo.getSSID(), scanResult.SSID) && TextUtils.equals(wifiInfo.getBSSID(), scanResult.BSSID)) {
                return scanResult;
            }
        }
        return null;
    }

    private void updateNetworkInfo(NetworkInfo networkInfo) {
        /* Sticky broadcasts can call this when wifi is disabled */
        if (!isWifiEnabled()) {
            return;
        }
        WifiInfo info = mWifiManager.getConnectionInfo();
//        Log.i(TAG, "updateNetworkInfo: " + info);
        ScanResult sr = findByWifiInfo(info);
        if (info != null) {
            currentWifiInfo.BSSID = info.getBSSID();
            String ssid = info.getSSID();
            if (!TextUtils.isEmpty(ssid)) {
                currentWifiInfo.SSID = ssid.replace("\"", "");
            } else {
                currentWifiInfo.SSID = null;
            }
            currentWifiInfo.level = info.getRssi();
            currentWifiInfo.frequency = info.getFrequency();
            if (sr != null) {
                currentWifiInfo.capabilities = sr.capabilities;
                currentWifiInfo.timestamp = sr.timestamp;
                currentWifiInfo.level = sr.level;
            }
        } else {
            currentWifiInfo.BSSID = null;
            currentWifiInfo.SSID = null;
        }
        if (mOnWifiChangedListener != null) {
            ThreadUtils.postOnMainThread(() -> {
                mOnWifiChangedListener.onCurrentWifiChanged(currentWifiInfo);
            });
        }

        if (networkInfo != null) {
            mLastNetworkInfo = networkInfo;
            if (DBG()) {
                Log.d(TAG, "mLastNetworkInfo set: " + mLastNetworkInfo);
            }

            if (networkInfo.isConnected() != mConnected.getAndSet(networkInfo.isConnected())) {
                mListener.onConnectedChanged();
            }
        }
    }
    //endregion


    /**
     * Receiver for handling broadcasts.
     * <p>
     * This receiver is registered on the WorkHandler.
     */
    final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            Log.i(TAG, "onReceive: " + action);
            if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(action)) {
                updateWifiState(
                        intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE,
                                WifiManager.WIFI_STATE_UNKNOWN));
            } else if (WifiManager.SCAN_RESULTS_AVAILABLE_ACTION.equals(action)) {
                mStaleScanResults = false;
                mLastScanSucceeded =
                        intent.getBooleanExtra(WifiManager.EXTRA_RESULTS_UPDATED, true);
                fetchScansAndConfigsAndUpdateAccessPoints();
            } /*else if (WifiManager.CONFIGURED_NETWORKS_CHANGED_ACTION.equals(action)
                    || WifiManager.ACTION_LINK_CONFIGURATION_CHANGED.equals(action)) {
                fetchScansAndConfigsAndUpdateAccessPoints();
            }*/ else if (WifiManager.NETWORK_STATE_CHANGED_ACTION.equals(action)) {
                // TODO(sghuman): Refactor these methods so they cannot result in duplicate
                // onAccessPointsChanged updates being called from this intent.
                NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO);
                updateNetworkInfo(info);
                fetchScansAndConfigsAndUpdateAccessPoints();
            } else if (WifiManager.RSSI_CHANGED_ACTION.equals(action)) {
                updateNetworkInfo(/* networkInfo= */ null);
            }
        }
    };

    /**
     * Handles updates to WifiState.
     *
     * <p>If Wifi is not enabled in the enabled state, {@link #mStaleScanResults} will be set to
     * true.
     */
    private void updateWifiState(int state) {
        if (isVerboseLoggingEnabled()) {
            Log.d(TAG, "updateWifiState: " + state);
        }
        if (state == WifiManager.WIFI_STATE_ENABLED) {
            synchronized (mLock) {
                if (mScanner != null) {
                    // We only need to resume if mScanner isn't null because
                    // that means we want to be scanning.
                    mScanner.resume();
                }
            }
        } else {
            mLastInfo = null;
            mLastNetworkInfo = null;
            synchronized (mLock) {
                if (mScanner != null) {
                    mScanner.pause();
                }
            }
            mStaleScanResults = true;
            //WIFI开关关闭处理
            if (state == WifiManager.WIFI_STATE_DISABLED) {
                currentWifiInfo.SSID = null;
                currentWifiInfo.BSSID = null;
                currentWifiInfo.capabilities = null;
                currentWifiInfo.level = 0;
                currentWifiInfo.frequency = 0;
                currentWifiInfo.timestamp = 0;
                if (mOnWifiChangedListener != null) {
                    ThreadUtils.postOnMainThread(() -> {
                        mOnWifiChangedListener.onCurrentWifiChanged(currentWifiInfo);
                        mOnWifiChangedListener.onWifiListChanged(Collections.emptyList());
                    });
                }
            }
        }
        mListener.onWifiStateChanged(state);
    }

    private final class WifiTrackerNetworkCallback extends ConnectivityManager.NetworkCallback {

        public void onCapabilitiesChanged(Network network, NetworkCapabilities nc) {
//            WifiInfo connectionInfo = mWifiManager.getConnectionInfo();

//            if (network.equals(mWifiManager.getCurrentNetwork())) {
            // TODO(sghuman): Investigate whether this comment still holds true and if it makes
            // more sense fetch the latest network info here:

            // We don't send a NetworkInfo object along with this message, because even if we
            // fetch one from ConnectivityManager, it might be older than the most recent
            // NetworkInfo message we got via a WIFI_STATE_CHANGED broadcast.
//                updateNetworkInfo(/* networkInfo= */ null);
//            }
        }
    }

    class Scanner extends Handler {
        static final int MSG_SCAN = 0;

        private int mRetry = 0;

        void resume() {
            if (isVerboseLoggingEnabled()) {
                Log.d(TAG, "Scanner resume");
            }
            if (!hasMessages(MSG_SCAN)) {
                sendEmptyMessage(MSG_SCAN);
            }
        }

        void pause() {
            if (isVerboseLoggingEnabled()) {
                Log.d(TAG, "Scanner pause");
            }
            mRetry = 0;
            removeMessages(MSG_SCAN);
        }

        boolean isScanning() {
            return hasMessages(MSG_SCAN);
        }

        @Override
        public void handleMessage(Message message) {
            if (message.what != MSG_SCAN) return;
            if (mWifiManager.startScan()) {
                mRetry = 0;
            } else if (++mRetry >= 3) {
                mRetry = 0;
//                if (mContext != null) {
//                    Toast.makeText(mContext, R.string.wifi_fail_to_scan, Toast.LENGTH_LONG).show();
//                }
                return;
            }
            sendEmptyMessageDelayed(MSG_SCAN, WIFI_RESCAN_INTERVAL_MS);
        }
    }

    /**
     * A restricted multimap for use in constructAccessPoints
     */
    private static class Multimap<K, V> {
        private final HashMap<K, List<V>> store = new HashMap<K, List<V>>();

        /**
         * retrieve a non-null list of values with key K
         */
        List<V> getAll(K key) {
            List<V> values = store.get(key);
            return values != null ? values : Collections.<V>emptyList();
        }

        void put(K key, V val) {
            List<V> curVals = store.get(key);
            if (curVals == null) {
                curVals = new ArrayList<V>(3);
                store.put(key, curVals);
            }
            curVals.add(val);
        }
    }

    /**
     * Wraps the given {@link WifiListener} instance and executes its methods on the Main Thread.
     *
     * <p>Also logs all callbacks invocations when verbose logging is enabled.
     */
    class WifiListenerExecutor implements WifiListener {

        private final WifiListener mDelegatee;

        public WifiListenerExecutor(WifiListener listener) {
            mDelegatee = listener;
        }

        @Override
        public void onWifiStateChanged(int state) {
            runAndLog(() -> mDelegatee.onWifiStateChanged(state),
                    String.format("Invoking onWifiStateChanged callback with state %d", state));
        }

        @Override
        public void onConnectedChanged() {
            runAndLog(mDelegatee::onConnectedChanged, "Invoking onConnectedChanged callback");
        }

        @Override
        public void onAccessPointsChanged() {
            runAndLog(mDelegatee::onAccessPointsChanged, "Invoking onAccessPointsChanged callback");
        }

        private void runAndLog(Runnable r, String verboseLog) {
            ThreadUtils.postOnMainThread(() -> {
                if (mRegistered) {
                    if (isVerboseLoggingEnabled()) {
                        Log.i(TAG, verboseLog);
                    }
                    r.run();
                }
            });
        }
    }

    /**
     * WifiListener interface that defines callbacks indicating state changes in WifiTracker.
     *
     * <p>All callbacks are invoked on the MainThread.
     */
    public interface WifiListener {
        /**
         * Called when the state of Wifi has changed, the state will be one of
         * the following.
         *
         * <li>{@link WifiManager#WIFI_STATE_DISABLED}</li>
         * <li>{@link WifiManager#WIFI_STATE_ENABLED}</li>
         * <li>{@link WifiManager#WIFI_STATE_DISABLING}</li>
         * <li>{@link WifiManager#WIFI_STATE_ENABLING}</li>
         * <li>{@link WifiManager#WIFI_STATE_UNKNOWN}</li>
         * <p>
         *
         * @param state The new state of wifi.
         */
        void onWifiStateChanged(int state);

        /**
         * Called when the connection state of wifi has changed and
         * {@link WifiTracker#isConnected()} should be called to get the updated state.
         */
        void onConnectedChanged();

        void onAccessPointsChanged();
    }

    /**
     * Invokes {@link WifiListenerExecutor#onAccessPointsChanged()} iif {@link #mStaleScanResults}
     * is false.
     */
    private void conditionallyNotifyListeners() {
        if (mStaleScanResults) {
            return;
        }

        mListener.onAccessPointsChanged();
    }

    /**
     * Filters unsupported networks from scan results. New WPA3 networks and OWE networks
     * may not be compatible with the device HW/SW.
     *
     * @param scanResults List of scan results
     * @return List of filtered scan results based on local device capabilities
     */
    private List<ScanResult> filterScanResultsByCapabilities(List<ScanResult> scanResults) {
        if (scanResults == null) {
            return null;
        }

        // Get and cache advanced capabilities
        boolean isOweSupported = false;
        boolean isSaeSupported = false;
        boolean isSuiteBSupported = false;

        if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
            isOweSupported = mWifiManager.isEnhancedOpenSupported();
            isSaeSupported = mWifiManager.isWpa3SaeSupported();
            isSuiteBSupported = mWifiManager.isWpa3SuiteBSupported();
        }

        List<ScanResult> filteredScanResultList = new ArrayList<>();

        // Iterate through the list of scan results and filter out APs which are not
        // compatible with our device.
        for (ScanResult scanResult : scanResults) {
            if (scanResult.capabilities.contains(WIFI_SECURITY_PSK)) {
                // All devices (today) support RSN-PSK or WPA-PSK
                // Add this here because some APs may support both PSK and SAE and the check
                // below will filter it out.
                filteredScanResultList.add(scanResult);
                continue;
            }

            if ((scanResult.capabilities.contains(WIFI_SECURITY_SUITE_B_192) && !isSuiteBSupported)
                    || (scanResult.capabilities.contains(WIFI_SECURITY_SAE) && !isSaeSupported)
                    || (scanResult.capabilities.contains(WIFI_SECURITY_OWE) && !isOweSupported)) {
                if (isVerboseLoggingEnabled()) {
                    Log.v(TAG, "filterScanResultsByCapabilities: Filtering SSID "
                            + scanResult.SSID + " with capabilities: " + scanResult.capabilities);
                }
            } else {
                // Safe to add
                filteredScanResultList.add(scanResult);
            }
        }

        return filteredScanResultList;
    }
}
